延續 Day 21「第一次建立 ASP.NET Core Web API」,這次開始規劃智慧醫院管理平台未來使用的 Oracle 關聯式資料庫。
前面已經完成主要 Angular 前端畫面,也成功建立第一支 ASP.NET Core Web API。
不過,目前醫師資料、預約掛號及我的預約等功能仍使用 Demo 資料,尚未真正儲存到資料庫。
因此今天希望先整理系統的資料需求,了解哪些資料需要長期保存,以及不同資料之間應該如何建立關係。
本次主要目標:
今天主要完成資料庫設計,不會直接建立 Oracle 資料庫,也不進行 ASP.NET Core 與 Oracle 的串接。
前面幾天的 Angular 預約流程已經可以操作:
醫師查詢
↓
醫師詳細資訊
↓
預約掛號
↓
預約成功
↓
我的預約
↓
預約詳細
↓
取消預約
但目前仍然存在一個問題:
網頁可以呈現預約結果,不代表資料已經真正儲存。
例如 Day 19 的預約成功頁面,只會暫時顯示使用者選擇的醫師、日期及時段。
Day 20 的我的預約,則使用兩筆固定的 Demo 資料。
取消預約後雖然可以看到狀態改變,但重新整理網頁,資料就會恢復原本的展示狀態。
因此正式系統需要一個能長期保存資料的位置。
資料庫除了儲存資料,也需要協助維持資料之間的一致性。
例如:
這些問題不能只靠前端頁面解決。
所以這次先不急著串接 Oracle,而是從資料需求與資料表關係開始規劃。
這次沒有直接憑空設計資料表,而是先讓 Codex 檢查目前已經完成的 Angular 頁面與資料服務。
主要檢查:
src/app/pages/
src/app/core/
src/app/app.routes.ts
backend/SmartCare.Api/
以及之前的開發紀錄、需求文件與 Demo 資料。
目前畫面包含:
但並不是每個畫面上的欄位都一定要存進資料庫。
例如確認密碼只是註冊時的輸入驗證,不需要和真正的密碼一起儲存。
另外,正式系統不能儲存明文密碼,而應保存使用安全密碼雜湊演算法產生的結果。
目前資料包含:
這些資料未來需要整理成醫師、科別、專長及門診等不同實體。
目前畫面包含:
這次也開始思考:預約資料究竟應該儲存醫師姓名、日期文字,還是連結到真正的門診紀錄?
目前有:
這些資料需要在預約建立後保存,不能只存在 Angular 的暫時狀態中。
經過盤點後,將資料區分為三種:
| 類型 | 範例 | 是否需要長期保存 |
|---|---|---|
| 業務資料 | 帳號、醫師、門診、預約 | 需要 |
| 前端操作狀態 | 目前選擇的篩選、編輯模式、彈窗狀態 | 不需要 |
| 可計算資料 | 預約筆數、剩餘名額 | 可由既有資料計算 |
這次學到,資料庫設計不是將畫面上的每個欄位直接變成資料庫欄位,而是先判斷資料真正的用途。
完成現況分析後,接著需要確認系統第一版的業務範圍。
這次主要確定三項設計:
目前先不實作家長替孩子、家屬替病人預約等情境。
因此第一版限定會員本人預約。
不過仍然將「使用者帳號」與「實際就診者」分成兩張資料表,保留之後擴充的可能性。
這次不規劃每位病人獨立的看診分鐘時槽。
例如某位醫師在特定日期的上午門診,有一個診次總名額。
使用者選擇的是該診次,而不是精確到上午 9:15 或 9:30 的個別時段。
目前醫師只對應一個主要科別。
因此不需要先建立醫師與科別的多對多關聯表。
這三項決定會直接影響後續資料表的結構。
根據前端功能與已確認的需求,最後規劃了七張資料表。
| 資料表 | 用途 |
|---|---|
| Users | 儲存登入帳號與會員聯絡方式 |
| Patients | 儲存實際就診者 |
| Departments | 儲存科別 |
| Doctors | 儲存醫師資料 |
| DoctorSchedules | 儲存具體門診時間與名額 |
| Appointments | 儲存預約紀錄及狀態 |
| DoctorSpecialties | 儲存醫師的多筆專長 |
這次沒有另外建立病歷、檢驗、處方或住院資料表。
因為目前第一階段是醫院預約掛號網站,不是完整的醫院資訊系統。
另外,也沒有為了增加資料表數量而提前建立大量未來功能。
我認為這次最重要的不是建立多少張表,而是每張表都應該有明確的用途。
這次第一個需要思考的問題,就是使用者帳號和就診者是否應該放在同一張資料表。
原本從前端畫面來看,登入、個人資料與預約者資料可能會讓人覺得都屬於同一個人。
但資料庫設計時,需要區分兩個概念。
Users:誰正在使用系統?
例如:
Patients:誰實際接受醫療服務?
例如:
本次資料表關係為:
Users
│
│ 一對零或一
↓
Patients
目前一個會員帳號最多對應一位就診者,而每位就診者必須對應一個會員帳號。
這是目前「會員本人預約」的設計。
未來如果要加入代約功能,就需要重新設計帳號、就診者與代理授權的關係。
| 欄位 | 用途 |
|---|---|
| user_id | 使用者主鍵 |
| username | 登入帳號 |
| 聯絡信箱 | |
| phone | 聯絡電話 |
| password_hash | 密碼雜湊 |
| created_at | 建立時間 |
| 欄位 | 用途 |
|---|---|
| patient_id | 就診者主鍵 |
| user_id | 對應會員帳號 |
| patient_name | 就診者姓名 |
這次也特別注意,不使用姓名、電話或身分證字號作為資料表主鍵。
因為這些欄位可能變更、重複或涉及敏感個資。
接下來規劃醫師相關資料。
目前醫師查詢頁有多個科別篩選選項。
資料庫中需要建立真正的科別清單,而不是讓每筆醫師資料都重複儲存科別名稱。
主要欄位:
Departments
├── department_id
└── department_name
其中 department_id 作為主鍵。
目前畫面中的「全部」只是篩選選項,不是實際的科別,因此不需要存進 Departments。
主要欄位:
Doctors
├── doctor_id
├── department_id
├── doctor_name
├── title
├── education
├── experience
└── illustration_path
其中 department_id 用來對應 Departments。
這樣每位醫師可以透過外鍵連結自己的主要科別。
目前每位醫師可能有多項專長。
如果直接在 Doctors 裡設計:
specialty1
specialty2
specialty3
就會限制專長數量,也不利於後續維護。
因此這次另外建立 DoctorSpecialties。
主要欄位:
DoctorSpecialties
├── doctor_id
└── specialty_name
每一列代表一位醫師的一項專長。
這次使用 doctor_id 和 specialty_name 作為複合主鍵,避免同一位醫師出現重複的專長文字。
這次我認為最重要的資料表之一,是 DoctorSchedules。
因為前端的醫師詳細頁與預約掛號頁面都有門診時間,但原本只是展示用資料。
正式預約時,需要知道使用者預約的是哪一天、哪位醫師的哪一個診次。
因此規劃:
DoctorSchedules
├── schedule_id
├── doctor_id
├── starts_at
├── ends_at
├── capacity
├── is_bookable
└── room
主要欄位:
| 欄位 | 用途 |
|---|---|
| schedule_id | 診次主鍵 |
| doctor_id | 對應醫師 |
| starts_at | 診次開始時間 |
| ends_at | 診次結束時間 |
| capacity | 診次總名額 |
| is_bookable | 是否開放預約 |
| room | 診間 |
例如只儲存:
醫師:D01
時段:上午
無法判斷是哪一天的上午門診。
而且未來要管理名額時,也無法知道預約應該扣除哪一個診次的名額。
因此需要使用 schedule_id 唯一識別具體診次。
原本前端醫師資料中有固定的診間資訊。
但實際上,同一位醫師在不同日期或不同診次,可能使用不同診間。
因此這次將 room 規劃在 DoctorSchedules,而不是 Doctors。
這樣資料結構比較符合診次的用途。
完成醫師與診次規劃後,接著設計最重要的預約資料表。
目前規劃:
Appointments
├── appointment_id
├── patient_id
├── schedule_id
├── appointment_status
├── visit_reason
├── reference_code
├── created_at
└── cancelled_at
主要欄位:
| 欄位 | 用途 |
|---|---|
| appointment_id | 唯一預約 ID |
| patient_id | 對應就診者 |
| schedule_id | 對應具體診次 |
| appointment_status | 預約狀態 |
| visit_reason | 就診原因 |
| reference_code | 對外顯示的預約編號 |
| created_at | 預約建立時間 |
| cancelled_at | 取消時間 |
這次決定 Appointments 只需要保存 schedule_id,而不是重複保存醫師名稱、科別與門診日期。
因為透過:
Appointments
↓ schedule_id
DoctorSchedules
↓ doctor_id
Doctors
↓ department_id
Departments
就可以找到對應的醫師與科別資料。
這樣可以避免同一筆資訊在多張資料表中重複出現。
目前設計不會直接刪除預約紀錄,而是修改狀態。
例如:
原本:
appointment_status = confirmed
取消後:
appointment_status = cancelled
cancelled_at = 取消時間
這樣可以保留原本的預約資料。
不過本次只是資料庫設計,還沒有真正實作取消 API。
完成初步資料表後,接著開始了解關聯式資料庫的重要觀念。
第一個是 Primary Key,也就是主鍵。
主鍵主要用來唯一識別資料表中的某一筆資料。
例如 Users:
| user_id | username |
|---|---|
| 1 | user01 |
| 2 | user02 |
| 3 | user03 |
其中 user_id 是主鍵。
即使使用者修改帳號名稱,user_id 仍然可以保留相同的值。
主鍵具有幾個重要特性:
這次大部分資料表都規劃使用獨立的數字 ID。
例如:
Users.user_id
Doctors.doctor_id
Appointments.appointment_id
但 DoctorSpecialties 比較不同,它使用兩個欄位組成複合主鍵:
(doctor_id, specialty_name)
這次讓我了解到,主鍵不一定只能由一個欄位組成。
第二個重要觀念是 Foreign Key。
外鍵用來建立資料表之間的參照關係。
例如:
Departments
department_id(PK)
│
↓
Doctors
department_id(FK)
Doctors 中的 department_id 必須對應到 Departments 裡存在的科別。
這樣可以避免醫師資料指向不存在的科別。
目前主要關聯:
| 父資料表 | 子資料表 | 關係 |
|---|---|---|
| Users | Patients | 一對零或一 |
| Departments | Doctors | 一對多 |
| Doctors | DoctorSpecialties | 一對多 |
| Doctors | DoctorSchedules | 一對多 |
| Patients | Appointments | 一對多 |
| DoctorSchedules | Appointments | 一對多 |
例如一位醫師可以有多筆門診排班。
Doctors
│
├── Schedule A
├── Schedule B
└── Schedule C
這是一對多的關係。
外鍵可以避免資料指向不存在的對象。
但外鍵不能保證:
這些問題需要之後透過後端規則與資料庫交易處理。
完成資料表與關聯後,接著使用 Mermaid 繪製 ERD。
ERD(Entity Relationship Diagram)是實體關聯圖,用來表示資料表以及彼此之間的關係。
之前的流程圖主要描述使用者操作順序,而 ERD 則著重在資料結構。
這次產出的 ERD:
erDiagram
USERS ||--o| PATIENTS : identifies
DEPARTMENTS ||--o{ DOCTORS : groups
DOCTORS ||--o{ DOCTOR_SPECIALTIES : describes
DOCTORS ||--o{ DOCTOR_SCHEDULES : provides
PATIENTS ||--o{ APPOINTMENTS : has
DOCTOR_SCHEDULES ||--o{ APPOINTMENTS : receives
USERS {
number user_id PK
varchar2 username UK
varchar2 email
varchar2 phone
varchar2 password_hash
timestamp_tz created_at
}
PATIENTS {
number patient_id PK
number user_id FK, UK
varchar2 patient_name
}
DEPARTMENTS {
number department_id PK
varchar2 department_name UK
}
DOCTORS {
number doctor_id PK
number department_id FK
varchar2 doctor_name
varchar2 title
varchar2 education
varchar2 experience
varchar2 illustration_path
}
DOCTOR_SPECIALTIES {
number doctor_id PK, FK
varchar2 specialty_name PK
}
DOCTOR_SCHEDULES {
number schedule_id PK
number doctor_id FK
timestamp starts_at
timestamp ends_at
number capacity
char is_bookable
varchar2 room
}
APPOINTMENTS {
number appointment_id PK
number patient_id FK
number schedule_id FK
varchar2 appointment_status
varchar2 visit_reason
varchar2 reference_code UK
timestamp_tz created_at
timestamp_tz cancelled_at
}
這次主要學習三個關聯符號:
| 符號 | 意義 |
|---|---|
| ` | |
| `o | ` |
o{ |
零個或多個 |
例如:
erDiagram
DOCTORS ||--o{ DOCTOR_SCHEDULES : provides
表示一位醫師可以有零筆或多筆門診排班,而每筆排班都必須屬於一位醫師。
這次繪製的 ERD 是初版設計,不代表 Oracle 中已經建立這些資料表。
前面使用 Angular 時,資料主要透過 TypeScript 型別或 JSON 呈現。
但在關聯式資料庫中,需要替每個欄位指定適當的資料型別。
由於未來預計使用 Oracle,所以這次先學習 Oracle 常見型別。
| 資料型別 | 用途 | 本次範例 |
|---|---|---|
NUMBER(12,0) |
整數數值 | user_id、doctor_id |
VARCHAR2(n CHAR) |
變動長度文字 | 姓名、科別 |
CHAR(1 CHAR) |
固定長度文字 | Y/N 狀態 |
DATE |
日期與時間 | 本版尚未採用 |
TIMESTAMP |
更精細的日期時間 | 診次開始、結束時間 |
CLOB |
大型文字 | 本版尚未使用 |
例如:
user_id NUMBER(12,0)
表示使用精度為 12、小數位數為 0 的數值型別。
本次使用於資料表 ID。
例如:
doctor_name VARCHAR2(100 CHAR)
用來儲存醫師姓名。
這次特別學到:
VARCHAR2(100 CHAR)
與:
VARCHAR2(100 BYTE)
並不完全相同。
前者以字元數為長度語意,後者以位元組為長度語意。
因為智慧醫院系統會使用中文,所以欄位長度的設計需要考慮字元與位元組的差異。
這次門診時間規劃使用:
starts_at TIMESTAMP(0)
ends_at TIMESTAMP(0)
用來表示具體日期與時間。
而預約建立時間及取消時間則採用帶時區的 TIMESTAMP 型別。
這次也了解到,Oracle 的 DATE 不只是年月日,還包含時分秒。
因此資料型別的選擇不能只看名稱,還需要了解它實際保存的資訊。
完成資料表規劃後,接著學習資料庫正規化。
正規化的目的是減少不必要的資料重複,避免新增、修改或刪除資料時產生矛盾。
假設所有資料都放在同一張預約表:
| 預約ID | 醫師姓名 | 科別 | 日期 |
|---|---|---|---|
| A001 | 醫師甲 | 內科 | 8/20 |
| A002 | 醫師甲 | 內科 | 8/21 |
| A003 | 醫師甲 | 內科 | 8/22 |
如果醫師姓名或科別需要修改,就可能需要更新多筆預約資料。
只要漏改其中一筆,就可能出現資料不一致。
因此這次將資料拆成不同的實體。
Departments
↓
Doctors
↓
DoctorSchedules
↓
Appointments
預約只需要透過外鍵連結其他資料表,就能取得需要顯示的醫師與門診資訊。
資料欄位應保存適合該資料領域的單一值,不應將多項可獨立處理的資料混在一起。
例如醫師專長不直接寫成:
specialties = "專長一,專長二,專長三"
而是使用 DoctorSpecialties,讓每項專長獨立成一筆資料。
非鍵欄位應依賴完整的候選鍵,而不是只依賴複合鍵的一部分。
例如 DoctorSpecialties 使用:
(doctor_id, specialty_name)
作為複合主鍵。
如果又將 doctor_name 放進 DoctorSpecialties,醫師姓名其實只依賴 doctor_id,不需要依賴完整複合鍵。
因此醫師姓名應保留在 Doctors。
非鍵資料不應透過其他非鍵欄位產生不必要的傳遞依賴。
例如預約連結門診,門診連結醫師,而醫師連結科別。
因此不需要在 Appointments 再存一份醫師姓名與科別名稱。
這次學到正規化不是為了增加資料表,而是讓不同資料有明確且一致的保存位置。
除了設計欄位與資料表關係,資料庫也需要設定限制,避免保存不合理的資料。
這次主要學習:
| 限制 | 用途 |
|---|---|
| PRIMARY KEY | 唯一識別資料 |
| FOREIGN KEY | 確保參照資料存在 |
| NOT NULL | 欄位不可缺少 |
| UNIQUE | 限制特定值重複 |
| CHECK | 限制欄位的合法內容 |
| DEFAULT | 設定預設值 |
例如:
CHECK (capacity > 0)
避免建立總名額為零或負數的診次。
例如:
CHECK (ends_at > starts_at)
避免資料表出現結束時間早於開始時間的診次。
目前第一版只規劃:
confirmed
cancelled
並透過 CHECK 限制狀態值。
其中 completed 雖然在前端篩選中存在,但正式完成看診的規則還沒有確認,因此這次沒有直接加入資料庫狀態。
例如:
Appointments.schedule_id
可以保證預約指向存在的診次。
但不能保證該診次還有名額。
另外,即使某筆預約 ID 存在,也不代表目前登入的使用者有權查看或取消。
這些規則需要未來在 ASP.NET Core 後端進一步處理。
完成七張資料表後,接著將它們對應回之前完成的 Angular 預約流程。
| 前端功能 | 未來主要使用的資料表 |
|---|---|
| 醫師查詢 | Departments、Doctors、DoctorSpecialties |
| 醫師詳細 | Doctors、DoctorSchedules |
| 選擇門診 | DoctorSchedules、Appointments |
| 確認預約 | Patients、DoctorSchedules、Appointments |
| 預約成功 | Appointments 及相關資料 |
| 我的預約 | Appointments 及關聯表 |
| 預約詳細 | Appointments 及關聯表 |
| 取消預約 | Appointments |
這次最重要的資料關係是:
預約 Appointments
↓
具體門診 DoctorSchedules
↓
醫師 Doctors
↓
科別 Departments
未來建立預約時,前端不應該只傳送:
{
"doctorName": "醫師甲",
"date": "2026/08/20",
"slot": "上午"
}
因為這些文字不一定能唯一識別某個診次。
更合理的方向是讓使用者選擇具體門診,再由後端根據該診次的 ID 驗證資料。
這次只是分析資料關係,還沒有實作正式的預約 API。
這次規劃 DoctorSchedules 時,新增了 capacity 欄位。
但沒有另外建立一個獨立的剩餘名額欄位。
原因是剩餘名額可以根據診次總名額與目前占用名額的預約數量計算。
例如:
診次總名額:20
已確認預約:15
剩餘名額:5
但這次也開始思考另一個問題:
如果兩個使用者同時看到只剩下一個名額,然後同時按下確認預約,會怎麼辦?
假設:
剩餘名額:1
使用者 A → 看到剩餘 1 人
使用者 B → 看到剩餘 1 人
兩人同時送出預約
如果只靠 Angular 判斷,就可能發生超額預約。
因此未來需要透過後端與資料庫交易,處理名額檢查與預約建立之間的一致性。
這次也了解到:
資料庫限制可以確保基本欄位正確,但真正的預約名額與並發問題,仍然需要後端業務邏輯。
目前沒有實作交易、鎖定或正式預約名額管理。
完成資料字典與 ERD 後,這次另外產生了一份 Oracle SQL DDL 草稿。
檔案位置:
docs/database-drafts/DAY22-SCHEMA-DRAFT.sql
DDL(Data Definition Language)主要用於定義資料庫結構。
例如:
CREATE TABLE ...
可以用來建立資料表。
這次的 SQL 草稿包含七張資料表及主要限制條件。
不過目前只是設計草稿,沒有連線 Oracle,也沒有真正執行建表。
因為資料字典與 ERD 主要協助理解資料結構,而 SQL DDL 可以進一步檢查欄位、型別與限制是否具備明確定義。
例如可以確認:
目前還有 Oracle 版本、ID 產生方式、預約防重規則等事項需要確認。
因此這份 SQL 不能直接視為已經完成、可以正式上線的資料庫。
這次主要是資料庫設計工作,沒有實際執行 Oracle,因此也沒有發生 Oracle 連線或 SQL 建表錯誤。
不過在設計驗證時,曾遇到 Mermaid 解析工具的執行環境問題。
第一次載入解析環境時,缺少 window.structuredClone,後續補上 Node.js 提供的對應功能後,完成 Mermaid 解析驗證。
這個問題發生在驗證工具環境,不代表 ERD 語法有錯誤。
另外,Git Repository 仍無法從目前資料夾辨識,因此沒有執行 Git 初始化或提交。
這次還有幾項值得記錄的決定:
這些都是目前第一版資料庫的設計,不代表已經成為正式醫院系統的最終規則。
本次使用 Codex 協助:
這次產出:
docs/
├── DATABASE-DAY22.md
├── DAY22-TECHNICAL-SUMMARY.md
├── day22-design-validation.json
│
└── database-drafts/
└── DAY22-SCHEMA-DRAFT.sql
驗證結果:
| 項目 | 結果 |
|---|---|
| 資料表 | 7 張 |
| 欄位 | 35 個 |
| Mermaid ERD | 解析成功 |
| ERD 與 DDL | 表格及欄位數量一致 |
| 既有檔案比對 | 124 個檔案無變更 |
| Angular UI 修改 | 無 |
| HelloController 修改 | 無 |
| Oracle 建表 | 尚未執行 |
| 資料庫連線 | 尚未進行 |
核心資料表設計:
智慧醫院管理平台初版規劃的七張資料表。

Mermaid ERD:
使用 Mermaid 繪製七張資料表之間的 ERD,呈現主鍵、外鍵及一對多關係。

Oracle DDL 草稿:
根據資料字典產生的 Oracle DDL 草稿,以 Appointments 預約資料表為例。

這次完成智慧醫院管理平台第一版的 Oracle 資料庫規劃,將前面 Angular 頁面中的 Demo 資料需求,整理成具有主鍵、外鍵與關聯的資料表設計。
目前規劃了七張資料表,並且透過 ERD 確認主要資料關係。
不過這些都還停留在設計階段,尚未建立真正的 Oracle 資料庫。
接下來需要繼續確認:
等需求與資料庫環境更明確後,再開始實際建立資料表及串接 ASP.NET Core Web API。
這次最大的收穫,是了解到資料庫設計不能只是將畫面欄位轉成資料表,而是需要先考慮資料的用途、關聯、重複性與完整性。
也讓我對未來智慧醫院的預約掛號流程,有了更完整的資料結構概念。